1 /**
2 This module implements a $(LINK2 http://dlang.org/template-mixin.html,
3 template mixin) containing a program to search a list of directories
4 for all .d files therein, then writes a D program to run all unit
5 tests in those files using unit_threaded. The program
6 implemented by this mixin only writes out a D file that itself must be
7 compiled and run.
8 
9 To use this as a runnable program, simply mix in and compile:
10 -----
11 #!/usr/bin/rdmd
12 import unit_threaded;
13 mixin genUtMain;
14 -----
15 
16 Generally however, this code will be used by the gen_ut_main
17 dub configuration via `dub run`.
18 
19 By default, genUtMain will look for unit tests in CWD
20 and write a program out to a temporary file. To change
21 the file to write to, use the $(D -f) option. To change what
22 directories to look in, simply pass them in as the remaining
23 command-line arguments.
24 
25 The resulting file is also a program that must be compiled and, when
26 run, will run the unit tests found. By default, it will run all
27 tests. To run one test or all tests in a particular package, pass them
28 in as command-line arguments.  The $(D -h) option will list all
29 command-line options.
30 
31 Examples (assuming the generated file is called $(D ut.d)):
32 -----
33 rdmd -unittest ut.d # run all tests
34 rdmd -unittest ut.d tests.foo tests.bar # run all tests from these packages
35 rdmd ut.d -h # list command-line options
36 -----
37 */
38 
39 module unit_threaded.runtime;
40 
41 import unit_threaded.from;
42 
43 mixin template genUtMain() {
44 
45     int main(string[] args) {
46         try {
47             writeUtMainFile(args);
48             return 0;
49         } catch (Exception ex) {
50             import std.stdio : stderr;
51 
52             stderr.writeln(ex.msg);
53             return 1;
54         }
55     }
56 }
57 
58 struct Options {
59     bool verbose;
60     string fileName;
61     string[] dirs;
62     bool help;
63     bool showVersion;
64     string[] includes;
65     string[] files;
66 
67     bool earlyReturn() @safe pure nothrow const {
68         return help || showVersion;
69     }
70 }
71 
72 Options getGenUtOptions(string[] args) {
73     import std.getopt;
74     import std.stdio : writeln;
75 
76     Options options;
77     auto getOptRes = getopt(args, "verbose|v", "Verbose mode.", &options.verbose,
78             "file|f", "The filename to write. Will use a temporary if not set.",
79             &options.fileName, "I", "Import paths as would be passed to the compiler",
80             &options.includes, "version", "Show version.", &options.showVersion,);
81 
82     if (getOptRes.helpWanted) {
83         defaultGetoptPrinter("Usage: gen_ut_main [options] [testDir1] [testDir2]...",
84                 getOptRes.options);
85         options.help = true;
86         return options;
87     }
88 
89     if (options.showVersion) {
90         writeln("unit_threaded.runtime version v0.6.1");
91         return options;
92     }
93 
94     options.dirs = args.length <= 1 ? ["."] : args[1 .. $];
95 
96     if (options.verbose) {
97         writeln(__FILE__, ": finding all test cases in ", options.dirs);
98     }
99 
100     return options;
101 }
102 
103 from!"std.file".DirEntry[] findModuleEntries(in Options options) {
104 
105     import std.algorithm : splitter, canFind, map, startsWith, filter;
106     import std.array : array, empty;
107     import std.file : DirEntry, isDir, dirEntries, SpanMode;
108     import std.path : dirSeparator, buildNormalizedPath;
109     import std.exception : enforce;
110 
111     // dub list of files, don't bother reading the filesystem since
112     // dub has done it already
113     if (!options.files.empty && options.dirs == ["."]) {
114         return dubFilesToAbsPaths(options.fileName, options.files).map!toDirEntry.array;
115     }
116 
117     DirEntry[] modules;
118     foreach (dir; options.dirs) {
119         enforce(isDir(dir), dir ~ " is not a directory name");
120         auto entries = dirEntries(dir, "*.d", SpanMode.depth);
121         auto normalised = entries.map!(a => buildNormalizedPath(a.name));
122 
123         bool isHiddenDir(string p) {
124             return p.startsWith(".");
125         }
126 
127         bool anyHiddenDir(string p) {
128             return p.splitter(dirSeparator).canFind!isHiddenDir;
129         }
130 
131         modules ~= normalised.filter!(a => !anyHiddenDir(a)).map!toDirEntry.array;
132     }
133 
134     return modules;
135 }
136 
137 auto toDirEntry(string a) {
138     import std.file : DirEntry;
139 
140     return DirEntry(removePackage(a));
141 }
142 
143 // package.d files will show up as foo.bar.package
144 // remove .package from the end
145 string removePackage(string name) {
146     import std.algorithm : endsWith;
147     import std.array : replace;
148 
149     enum toRemove = "/package.d";
150     return name.endsWith(toRemove) ? name.replace(toRemove, "") : name;
151 }
152 
153 string[] dubFilesToAbsPaths(in string fileName, in string[] files) {
154     import std.algorithm : filter, map;
155     import std.array : array;
156     import std.path : buildNormalizedPath;
157 
158     // dub list of files, don't bother reading the filesystem since
159     // dub has done it already
160     return files.filter!(a => a != fileName).map!(a => removePackage(a))
161         .map!(a => buildNormalizedPath(a)).array;
162 }
163 
164 string[] findModuleNames(in Options options) {
165     import std.path : dirSeparator, stripExtension, absolutePath, relativePath;
166     import std.algorithm : endsWith, startsWith, filter, map;
167     import std.array : replace, array;
168     import std.path : baseName, absolutePath;
169 
170     // if a user passes -Isrc and a file is called src/foo/bar.d,
171     // the module name should be foo.bar, not src.foo.bar,
172     // so this function subtracts import path options
173     string relativeToImportDirs(string path) {
174         foreach (string importPath; options.includes) {
175             importPath = relativePath(importPath);
176             if (!importPath.endsWith(dirSeparator))
177                 importPath ~= dirSeparator;
178             if (path.startsWith(importPath)) {
179                 return path.replace(importPath, "");
180             }
181         }
182 
183         return path;
184     }
185 
186     return findModuleEntries(options).filter!(a => a.baseName != "reggaefile.d")
187         .filter!(a => a.absolutePath != options.fileName.absolutePath).map!(a => relativeToImportDirs(a.name))
188         .map!(a => replace(a.stripExtension, dirSeparator, ".")).array;
189 }
190 
191 string writeUtMainFile(string[] args) {
192     auto options = getGenUtOptions(args);
193     return writeUtMainFile(options);
194 }
195 
196 string writeUtMainFile(Options options) {
197     if (options.earlyReturn) {
198         return options.fileName;
199     }
200 
201     return writeUtMainFile(options, findModuleNames(options));
202 }
203 
204 private string writeUtMainFile(Options options, in string[] modules) {
205     import std.path : buildPath, dName = dirName;
206     import std.stdio : writeln, File;
207     import std.file : tempDir, getcwd, mkdirRecurse, exists;
208     import std.algorithm : map;
209     import std.array : join;
210 
211     if (!options.fileName) {
212         options.fileName = buildPath(tempDir, getcwd[1 .. $], "ut.d");
213     }
214 
215     if (!haveToUpdate(options, modules)) {
216         if (options.verbose)
217             writeln("Not writing to ", options.fileName, ": no changes detected");
218         return options.fileName;
219     } else {
220         if (options.verbose)
221             writeln("Writing to unit test main file ", options.fileName);
222     }
223 
224     const dirName = options.fileName.dName;
225     dirName.exists || mkdirRecurse(dirName);
226 
227     auto wfile = File(options.fileName, "w");
228     wfile.write(modulesDbList(modules));
229     wfile.writeln(q{
230 //Automatically generated by unit_threaded.gen_ut_main, do not edit by hand.
231 import unit_threaded;
232 });
233 
234     wfile.writeln("int main(string[] args)");
235     wfile.writeln("{");
236 
237     immutable indent = "                          ";
238     wfile.writeln("    return args.runTests!(\n" ~ modules.map!(a => indent ~ `"` ~ a ~ `"`)
239             .join(",\n") ~ "\n" ~ indent ~ ");");
240     wfile.writeln("}");
241     wfile.close();
242 
243     return options.fileName;
244 }
245 
246 private bool haveToUpdate(in Options options, in string[] modules) {
247     import std.file : exists;
248     import std.stdio : File;
249     import std.array : join;
250     import std..string : strip;
251 
252     if (!options.fileName.exists) {
253         return true;
254     }
255 
256     auto file = File(options.fileName);
257     return file.readln.strip != modulesDbList(modules);
258 }
259 
260 //used to not update the file if the file list hasn't changed
261 private string modulesDbList(in string[] modules) @safe pure nothrow {
262     import std.array : join;
263 
264     return "//" ~ modules.join(",");
265 }